package nl.uva.science.luna.pico.compiler;

import java.io.InputStream;
import java.io.FileOutputStream;

import aterm.pure.PureFactory;
import aterm.ATermFactory;
import aterm.ATerm;
import aterm.ATermList;

import nl.uva.science.luna.pico.language.Factory;

import nl.uva.science.luna.pico.language.types.PROGRAM;
import nl.uva.science.luna.pico.language.types.EXP;

import nl.uva.science.luna.pico.language.types.DECLS;
import nl.uva.science.luna.pico.language.types.DECLARATIONS;
import nl.uva.science.luna.pico.language.types.IDTYPEList;
import nl.uva.science.luna.pico.language.types.IDTYPE;
import nl.uva.science.luna.pico.language.types.TYPE;
import nl.uva.science.luna.pico.language.types.PICOID;

import nl.uva.science.luna.pico.language.types.STATEMENTList;
import nl.uva.science.luna.pico.language.types.STATEMENTS;
import nl.uva.science.luna.pico.language.types.STATEMENT;

/**
 * @author Sander Vellinga
 *
 * This class compiles the pico code to stack code
 */
public final class Compiler {
	private static Compiler m_compiler;
	
	private Factory m_picoFactory;	
	private ATermFactory m_factory;
	
	private SymbolTable m_symbolTable;
	
	private ATermList m_stackProgram;
	private int m_InstrLabel;
	
	/**
	 * Declare private constructor.
	 * An object of this class can be retrieved through getInstance().
	 */
	private Compiler() {
		m_picoFactory = Factory.getInstance(new PureFactory());
		m_symbolTable = new SymbolTable();
		
		m_factory = new PureFactory();
		
		// This list will hold the stackstatements
		m_stackProgram = (ATermList)m_factory.make("[[1, 0]]");
		m_InstrLabel = 2;
	}
	
	/**
	 * There can only be one compiler active at a time,
	 * so we use the singleton pattern to retrieve
	 * an object of this class.
	 * 
	 * @return Object of Compiler class.
	 */
	public static Compiler getInstance() {
		if(m_compiler == null) {
			m_compiler = new Compiler();
		}
		
		return m_compiler;
	}

	/**
	 * This is the main method of this class and starts the actual compiling
	 * 
	 * @param inputStream
	 */
	public void execute(InputStream inputStream) {	
		try {
			System.out.println("Compiler started");
			PROGRAM picoProgram = m_picoFactory.PROGRAMFromFile(inputStream);
		
			System.out.println("Reading variables...");
			DECLS decls = picoProgram.getDeclarations();
			this.loadVariablesFromDecls(decls);
			System.out.println("done");
			
			System.out.println("Reading statements...");
			STATEMENTS statements = picoProgram.getBody();
			STATEMENTList statementList = statements.getList();	
			this.loopStatements(statementList);
			
			// Add end of program instruction
			this.addStatementToStackProgram(m_factory.make("[<int>, stop]", m_InstrLabel));
			System.out.println("done");
			
			System.out.println("Writing to file...");
			System.out.println(m_stackProgram);
			// Write file to disk
			m_stackProgram.writeToTextFile(new FileOutputStream("stack.trm"));
			System.out.println("done");
		}
		catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
	}

	/**
	 * This method saves all declared variables in a symbol table
	 * so we can use them at a later stage
	 * 
	 * @param decls
	 */
	private void loadVariablesFromDecls(DECLS decls) {
		try {
			DECLARATIONS declarations = decls.getDeclarations();
			IDTYPEList idTypeList = declarations.getList();
			
			for (int i = 0; i < idTypeList.getLength(); i++) {
				IDTYPE variable = idTypeList.getIDTYPEAt(i);
				PICOID variableName = variable.getVariableName();
				
				// Store the variable in the symbol table so we have a location for it
				int varLocation = m_symbolTable.put(variableName);

				// Initialize all the variables in the stackProgram so they have a default value
				// This is needed because it can be possible that you want to do something 
				// with a variable that's not set yet. (ie. s := s || "#" )
				TYPE varType = variable.getVariableType();
				this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, varLocation));
				if (varType.isNatural()) {
					this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, 0));
				}
				else if (varType.isString()){
					this.addStatementToStackProgram(m_factory.make("[<int>, <str>]", m_InstrLabel, ""));
				}
				else {
					this.exitProgramOnError("Error: Unknown variabletype in loadVariablesFromDecls");
				}
				this.addStatementToStackProgram(m_factory.make("[<int>, store]", m_InstrLabel));			
				
			}
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
	}
	
	/**
	 * This method loops through all statements in a program
	 * 
	 * @param statementList
	 */
	private void loopStatements(STATEMENTList statementList) {
		try {
			STATEMENT statement;
			for (int i = 0; i < statementList.getLength(); i++) {
				statement = statementList.getSTATEMENTAt(i);
				
				if (statement.isAssignment()) {
					this.makeStackAssignmentInstructions(statement);
				}
				else if (statement.isLoop()) {
					this.makeStackLoopInstructions(statement);
				}
				else if (statement.isConditional()) {
					this.makeStackConditionalInstructions(statement);
				}
				else {
					this.exitProgramOnError("Error: Unknown statementtype in loopStatements");
				}
			}
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
	}
	
	/**
	 * This method makes the stack instructions for a assignment statement
	 * 
	 * @param statement
	 */
	private void makeStackAssignmentInstructions (STATEMENT statement) {
		try {
			PICOID variableName = statement.getPICOID();
			int location = m_symbolTable.get(variableName);
			this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, location));

			EXP expression = statement.getEXP();
			if (expression.isVariable() || expression.isNumberValue() || expression.isStringValue()) {
				this.makeExpressionInstructions(expression);
			}
			else if (expression.isAdd()) {
				this.makeAddInstructions(expression);
			}
			else if (expression.isSubtract()) {
				this.makeSubtractInstructions(expression);
			}
			else if (expression.isConcatenate()) {
				this.makeConcatInstructions(expression);
			}
			else {
				this.exitProgramOnError("Error: Unknown expressiontype in makeStackAssignmentInstructions");
			}
			
			this.addStatementToStackProgram(m_factory.make("[<int>, store]", m_InstrLabel));
			
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}	
	}
	
	/**
	 * This method makes the stack instructions for a add expression
	 * 
	 * @param expression
	 */
	private void makeAddInstructions (EXP expression) {
		try {
			this.makeExpressionInstructions(expression.getLeft());
			this.makeExpressionInstructions(expression.getRight());
			this.addStatementToStackProgram(m_factory.make("[<int>, add]", m_InstrLabel));
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}		
	}

	/**
	 * This method makes the stack instructions for a subtract expression
	 * 
	 * @param expression
	 */
	private void makeSubtractInstructions (EXP expression) {
		try {
			this.makeExpressionInstructions(expression.getLeft());
			this.makeExpressionInstructions(expression.getRight());
			this.addStatementToStackProgram(m_factory.make("[<int>, minus]", m_InstrLabel));
			this.addStatementToStackProgram(m_factory.make("[<int>, add]", m_InstrLabel));
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
	}
	
	/**
	 * This method makes the stack instructions for a concat expression
	 * 
	 * @param expression
	 */
	private void makeConcatInstructions (EXP expression) {
		try {
			this.makeExpressionInstructions(expression.getLeft());
			this.makeExpressionInstructions(expression.getRight());
			this.addStatementToStackProgram(m_factory.make("[<int>, concat]", m_InstrLabel));
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
	}

	/**
	 * Makes the instructions for the left and right side of an add, subtract or concat expression
	 * 
	 * @param expression
	 */
	private void makeExpressionInstructions(EXP expression) {
		try {
			if (expression.isVariable()) {
				int variableLocation = m_symbolTable.get(expression.getVariableName());
				this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, variableLocation));
				this.addStatementToStackProgram(m_factory.make("[<int>, load]", m_InstrLabel));			
			}
			else if (expression.isNumberValue()) {			
				int expressionValue = expression.getNumberValue();
				this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, expressionValue));
			}
			else if (expression.isStringValue()) {
				String expressionValue = expression.getStringValue();
				this.addStatementToStackProgram(m_factory.make("[<int>, <str>]", m_InstrLabel, expressionValue));
			}
			else {
				this.exitProgramOnError("Error: Unknown expressiontype in makeExpressionInstructions");
			}
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
		
	}
	
	/**
	 * This method makes the stack instructions for a loop statement
	 * 
	 * @param statement
	 */
	private void makeStackLoopInstructions (STATEMENT statement) {
		try {
			int loopStatementLocation = m_InstrLabel;
			EXP expression = statement.getCondition();
			
			this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, 0));
			int jumpOverLoopChildLocation = m_stackProgram.getChildCount()-1;
			
			// The loop condition is either a variable or a subtract action.
			// The loop continues as long as the subtract result or the variable 
			// doesn't equals 0
			if (expression.isVariable()) {
				this.makeExpressionInstructions(expression);
			} 
			else if (expression.isSubtract()) {
				this.makeSubtractInstructions(expression);
			}
			else {
				this.exitProgramOnError("Error: incorrect while condition");
			}
			
			this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, 0));
			this.addStatementToStackProgram(m_factory.make("[<int>, ifeq]", m_InstrLabel));
			
			// Execute loop body
			STATEMENTS loopStatements = statement.getBody();
			STATEMENTList loopStatementList = loopStatements.getList();
			this.loopStatements(loopStatementList);
			
			// Set a goto statement at end of loop statements so we have in fact a loop
			this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, loopStatementLocation));
			this.addStatementToStackProgram(m_factory.make("[<int>, goto]", m_InstrLabel));				
			
			// Reset jump location so we can skip the loop when the loop condition is false
			this.changeStatementFromStackProgram(m_factory.make("[<int>, <int>]", loopStatementLocation, m_InstrLabel), jumpOverLoopChildLocation);
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}		
	}
	
	/**
	 * This method makes the stack instructions for a conditional statement
	 * 
	 * @param statement
	 */
	private void makeStackConditionalInstructions (STATEMENT statement) {
		try {
			int ifStatementLocation = m_InstrLabel;
			EXP expression = statement.getCondition();
			
			this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, 0));
			int jumpToIfBranchChildLocation = m_stackProgram.getChildCount()-1;
			
			this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, 0));
			
			// The if condition checks if a variable doesn't equals with 0
			if (expression.isVariable()) {
				this.makeExpressionInstructions(expression);
			}
			else {
				this.exitProgramOnError("Error: incorrect if statement");
			}
			
			this.addStatementToStackProgram(m_factory.make("[<int>, ifl]", m_InstrLabel));

			/*
			 * EXECUTE ELSE BRANCH
			 */
			STATEMENTS elseBranch = statement.getElseBranch();
			STATEMENTList loopStatementList = elseBranch.getList();
			this.loopStatements(loopStatementList);
			
			// Set a goto at end of else branch so we can skip the if branch
			int endOfElseStatementLocation = m_InstrLabel;
			this.addStatementToStackProgram(m_factory.make("[<int>, <int>]", m_InstrLabel, 0));
			int jumpOutOfElseChildLocation = m_stackProgram.getChildCount()-1;
			this.addStatementToStackProgram(m_factory.make("[<int>, goto]", m_InstrLabel));	
			
			// Reset the goto at beginning of the if statement so we can jump  to the if branch (and this way skip the else branch)
			this.changeStatementFromStackProgram(m_factory.make("[<int>, <int>]", ifStatementLocation, m_InstrLabel), jumpToIfBranchChildLocation );		
			
			/*
			 * EXECUTE IF BRANCH
			 */
			STATEMENTS ifBranch = statement.getIfBranch();
			STATEMENTList ifStatementList = ifBranch.getList();
			this.loopStatements(ifStatementList);
			
			// Reset the goto at end of else branch so we can skip the if branch
			this.changeStatementFromStackProgram(m_factory.make("[<int>, <int>]", endOfElseStatementLocation, m_InstrLabel), jumpOutOfElseChildLocation );
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
	}
	
	/**
	 * This method add a single statement to the stackProgram
	 * and updates the instruction label value
	 * 
	 * @param statement
	 */
	private void addStatementToStackProgram(ATerm statement) {
		try {
			m_stackProgram = m_stackProgram.append(statement);
			m_InstrLabel++;
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
	}
	
	/**
	 * This method changes an existing statement in the stackProgram
	 * 
	 * @param statement
	 */
	private void changeStatementFromStackProgram(ATerm statement, int childLocation) {
		try {
			m_stackProgram = m_stackProgram.replace(statement, childLocation);
		} catch (Exception e) {
			this.exitProgramOnError(e.getMessage());
		}
	}
	
	/**
	 * This method prints a message before shutting down the program
	 * can be called when unexpected exception occurs
	 *  
	 * @param message
	 */
	private void exitProgramOnError(String message) {
		System.err.println(message);
		System.exit(0);
	}
}